package no.sachmuld.methodcombination.api;

import no.sachmuld.methodcombination.impl.validate.MethodValidationUtils;
import no.sachmuld.methodcombination.impl.reflection.MethodCombinationInvocationException;
import no.sachmuld.methodcombination.impl.reflection.MethodNotFoundException;
import org.apache.log4j.Logger;

import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.util.Arrays;

/**
 *
 */
public class BehaviourMethod {

    private static Logger logger = Logger.getLogger(BehaviourMethod.class);

    private Class behaviourClass;
    private Method method;
    private String methodName;
    private boolean required;
    private Class[] parameterTypes;
    private Class returnType;
    private Method targetMethod;

    public BehaviourMethod(Class behaviourClass, Method method, String methodName, boolean required, Class[] parameterTypes, Class returnType, Method targetMethod) {
        this.behaviourClass = behaviourClass;
        this.method = method;
        this.methodName = methodName;
        this.required = required;
        this.parameterTypes = parameterTypes;
        this.returnType = returnType;
        this.targetMethod = targetMethod;
    }

    public boolean isRequired() {
        return required;
    }

    public boolean exists() {
        return method != null;
    }

    public boolean hasValidReturnType() {
        if (!exists()) {
            return false;
        } else if (returnType == null) {
            return true;
        } else {
            return MethodValidationUtils.validateReturnType(method.getReturnType(), returnType);
        }
    }

    public boolean hasValidExceptionTypes() {
        if (!exists()) {
            return false;
        } else {
            return MethodValidationUtils.validateExceptionTypes(targetMethod.getExceptionTypes(), method.getExceptionTypes());
        }
    }

    public Method getMethod() {
        return method;
    }

    public Class<?> getBehaviourClass() {
        return behaviourClass;
    }

    public String getMethodName() {
        return methodName;
    }

    public Class<?>[] getParameterTypes() {
        return parameterTypes;
    }

    public Class getReturnType() {
        return returnType;
    }

    public Object invoke(Object behaviour, Object[] parameters, Object... extraParameters) {
        if (method == null) {
            throw new MethodNotFoundException();
        }
        try {
            if (!method.isAccessible()) {
                try {
                    method.setAccessible(true);
                } catch (SecurityException e) {
                    logger.warn(e);
                }
            }
            return method.invoke(behaviour, cat(parameters, extraParameters));
        } catch (IllegalAccessException e) {
            throw new RuntimeException(e);
        } catch (InvocationTargetException e) {
            Throwable cause = e.getCause();
            if (cause instanceof RuntimeException) {
                throw (RuntimeException) cause;
            } else {
                throw new MethodCombinationInvocationException(e.getCause());
            }
        }
    }


    public static class Builder {

        private Class behaviourClass;
        private Method targetMethod;
        private boolean required;
        private String prefix;
        private String name;
        private Class[] extraParameterTypes = {};
        private Class returnType;
        private boolean unknownReturnType = false;

        public Builder(Class behaviourClass, Method targetMethod, boolean required) {
            this.behaviourClass = behaviourClass;
            this.targetMethod = targetMethod;
            this.required = required;
        }

        public Builder prefix(String prefix) {
            this.prefix = prefix;
            return this;
        }

        public Builder name(String name) {
            this.name = name;
            return this;
        }

        public Builder extraParameterTypes(Class... extraParameterTypes) {
            this.extraParameterTypes = extraParameterTypes;
            return this;
        }

        public Builder returnType(Class returnType) {
            this.returnType = returnType;
            return this;
        }

        public Builder unknownReturnType() {
            this.unknownReturnType = true;
            return this;
        }

        public BehaviourMethod build() {
            String methodName = name == null ? targetMethod.getName() : name;
            methodName = prefix == null ? methodName : prefixedMethodName(methodName, prefix);
            Class[] allParameterTypes = cat(targetMethod.getParameterTypes(), extraParameterTypes);
            Method method;
            try {
                method = behaviourClass.getDeclaredMethod(methodName, allParameterTypes);
            } catch (NoSuchMethodException e) {
                method = null;
            }
            Class theReturnType = unknownReturnType ? null : (returnType == null ? targetMethod.getReturnType() : returnType);
            return new BehaviourMethod(behaviourClass, method, methodName, required, allParameterTypes, theReturnType, targetMethod);
        }
    }

    private static String prefixedMethodName(String methodName, String prefix) {
        return prefix + methodName.substring(0,1).toUpperCase() + methodName.substring(1);
    }

    private static Class[] cat(Class[] classes, Class... extras) {
        final Class[] result = Arrays.copyOf(classes, classes.length + extras.length);
        System.arraycopy(extras, 0, result, classes.length, extras.length);
        return result;
    }

    private static Object[] cat(Object[] objects, Object... extras) {
        final Object[] result = Arrays.copyOf(objects, objects.length + extras.length);
        System.arraycopy(extras, 0, result, objects.length, extras.length);
        return result;
    }

}
